import ctypes
import os
from typing import Optional, Union

from .camera import AcquisitionMode, EMGainMode, EmccdCamera, ReadMode, ShutterMode, ShutterTTLSignal, Status
from .dll import AC_SETFUNCTION_BASELINECLAMP, AC_SETFUNCTION_BASELINEOFFSET, ANDORCAPS, DRV_NOT_INITIALIZED, \
    DRV_SUCCESS, EmccdDll


class EmccdCameraByDll(EmccdCamera):
    @classmethod
    def from_path(cls, dll_path: Optional[str]) -> 'EmccdCameraByDll':
        dll_path = find_dll_in_path(dll_path or "atmcd64d.dll")
        dll = EmccdDll(dll_path)
        return EmccdCameraByDll(dll)

    def __init__(self, dll: EmccdDll):
        # noinspection PyTypeChecker
        self._dll: EmccdDll = ReturnCodeWrapper(dll)

        super().__init__()
        self._capabilities = self._get_capabilities()

    # dll

    @property
    def dll(self) -> EmccdDll:
        return self._dll

    @property
    def raw_dll(self) -> EmccdDll:
        # noinspection PyUnresolvedReferences,PyProtectedMember
        return self.dll._raw

    # lifecycle

    def _initialize(self):
        self._dll.Initialize(os.curdir.encode())

    def _shutdown(self):
        try:
            self._dll.ShutDown()
        except ReturnCodeError as err:
            # ignore "not initialized" error
            if err.rc != DRV_NOT_INITIALIZED:
                raise err

    # information

    def _get_capabilities(self) -> ANDORCAPS:
        caps = ANDORCAPS()
        self._dll.GetCapabilities(ctypes.byref(caps))
        return caps

    def get_head_model(self) -> str:
        model = ctypes.create_string_buffer(64)
        self._dll.GetHeadModel(model)
        return model.value.decode()

    def get_detector_size(self) -> tuple[int, int]:
        x = ctypes.c_int()
        y = ctypes.c_int()
        self._dll.GetDetector(ctypes.byref(x), ctypes.byref(y))
        return x.value, y.value

    # temperature

    def get_temperature_range(self) -> tuple[int, int]:
        min_temperature = ctypes.c_int()
        max_temperature = ctypes.c_int()
        self._dll.GetTemperatureRange(ctypes.byref(min_temperature), ctypes.byref(max_temperature))
        return min_temperature.value, max_temperature.value

    def set_temperature(self, temperature: int):
        self._dll.SetTemperature(temperature)

    def get_temperature(self) -> int:
        temperature = ctypes.c_int()
        self.raw_dll.GetTemperature(ctypes.byref(temperature))
        return temperature.value

    def set_cooler(self, on: bool):
        if on:
            self._dll.CoolerON()
        else:
            self._dll.CoolerOFF()

    # shutter

    def set_shutter(self, signal: ShutterTTLSignal, mode: ShutterMode, closing_time: int, opening_time: int):
        self._dll.SetShutter(signal.value, mode.value, closing_time, opening_time)

    # pre-amp (pre-amplifier)

    def get_pre_amp_gains_num(self) -> int:
        num = ctypes.c_int()
        self._dll.GetNumberPreAmpGains(ctypes.byref(num))
        return num.value

    def get_pre_amp_gain(self, index: int) -> float:
        """ gain factor """
        gain = ctypes.c_float()
        self._dll.GetPreAmpGain(index, ctypes.byref(gain))
        return gain.value

    def set_pre_amp_gain(self, index: int):
        self._dll.SetPreAmpGain(index)

    # em (electron multiplying)

    def set_em_advanced(self, on: bool):
        self._dll.SetEMAdvanced(int(on))

    def get_em_advanced(self) -> bool:
        em_advanced = ctypes.c_int()
        self._dll.GetEMAdvanced(ctypes.byref(em_advanced))
        return bool(em_advanced.value)

    def set_em_gain_mode(self, mode: EMGainMode):
        self._dll.SetEMGainMode(mode.value)

    def get_em_gain_range(self) -> tuple[int, int]:
        min_em_gain = ctypes.c_int()
        max_em_gain = ctypes.c_int()
        self._dll.GetEMGainRange(ctypes.byref(min_em_gain), ctypes.byref(max_em_gain))
        return min_em_gain.value, max_em_gain.value

    def set_em_gain(self, em_gain: int):
        self._dll.SetEMCCDGain(em_gain)

    def get_em_gain(self) -> int:
        em_gain = ctypes.c_int()
        self._dll.GetEMCCDGain(ctypes.byref(em_gain))
        return em_gain.value

    # baseline

    @property
    def support_baseline_clamp(self) -> bool:
        return bool(self._capabilities.ulSetFunctions & AC_SETFUNCTION_BASELINECLAMP)

    def set_baseline_clamp(self, enable: bool):
        if enable:
            self._dll.SetBaselineClamp(1)
        else:
            self._dll.SetBaselineClamp(0)

    @property
    def support_baseline_offset(self) -> bool:
        return bool(self._capabilities.ulSetFunctions & AC_SETFUNCTION_BASELINEOFFSET)

    def set_baseline_offset(self, offset: int):
        self._dll.SetBaselineOffset(offset)

    # vertical shift speeds

    def get_vs_speeds_num(self) -> int:
        num = ctypes.c_uint()
        self._dll.GetNumberVSSpeeds(ctypes.byref(num))
        return num.value

    def get_vs_speed(self, vs_speed_index: int) -> float:
        """  vertical shift speed (ms/pixel) """
        vs_speed = ctypes.c_float()
        self._dll.GetVSSpeed(vs_speed_index, ctypes.byref(vs_speed))
        return vs_speed.value

    def set_vs_speed(self, vs_speed_index: int):
        self._dll.SetVSSpeed(vs_speed_index)

    def get_fastest_recommended_vs_speed(self) -> tuple[int, float]:
        """

        Returns:
            vs_speed_index: int
            speed: float, ms/pixel
        """
        vs_speed_index = ctypes.c_int()
        vs_speed = ctypes.c_float()
        self._dll.GetFastestRecommendedVSSpeed(ctypes.byref(vs_speed_index), ctypes.byref(vs_speed))
        return vs_speed_index.value, vs_speed.value

    # ad channels & horizontal shift speeds

    def get_ad_channels_num(self) -> int:
        num = ctypes.c_int()
        self._dll.GetNumberADChannels(ctypes.byref(num))
        return num.value

    def get_hs_speeds_num(self, ad_channel_index: int, amplification: int) -> int:
        num = ctypes.c_int()
        self._dll.GetNumberHSSpeeds(ad_channel_index, amplification, ctypes.byref(num))
        return num.value

    def get_hs_speed(self, ad_channel_index: int, amplification: int, hs_speed_index: int) -> float:
        """ Horizontal shift speed (MHz) """
        speed = ctypes.c_float()
        self._dll.GetHSSpeed(ad_channel_index, amplification, hs_speed_index, ctypes.byref(speed))
        return speed.value

    def set_ad_channel(self, ad_channel_index: int):
        self._dll.SetADChannel(ad_channel_index)

    def set_hs_speed(self, amplification: int, hs_speed_index: int):
        self._dll.SetHSSpeed(amplification, hs_speed_index)

    # read mode & image area

    def set_read_mode(self, mode: ReadMode):
        self._dll.SetReadMode(mode.value)

    def set_image(self, *, hbin: int, vbin: int, hstart: int, hend: int, vstart: int, vend: int):
        # sdk 里的 start 和 end 都是 inclusive 的，并且从 1 开始计
        # 为了迎合编程习惯，此处改为从 0 开始计，并且 end 是 exclusive 的
        self._dll.SetImage(hbin, vbin, hstart + 1, hend, vstart + 1, vend)

    # acquisition mode & exposure & cycle

    def set_acquisition_mode(self, mode: AcquisitionMode):
        self._dll.SetAcquisitionMode(mode.value)

    def set_exposure_time(self, time: float):
        self._dll.SetExposureTime(time)

    def set_kinetic_cycle_time(self, time: float):
        self._dll.SetKineticCycleTime(time)

    def get_acquisition_times(self) -> tuple[float, float, float]:
        exposure_time = ctypes.c_float()
        accumulate_cycle_time = ctypes.c_float()
        kinetic_cycle_time = ctypes.c_float()
        self._dll.GetAcquisitionTimings(
            ctypes.byref(exposure_time), ctypes.byref(accumulate_cycle_time), ctypes.byref(kinetic_cycle_time))
        return exposure_time.value, accumulate_cycle_time.value, kinetic_cycle_time.value

    # acquisition control

    def start_acquisition(self):
        self._dll.StartAcquisition()

    def abort_acquisition(self):
        self._dll.AbortAcquisition()

    def get_status(self) -> Status:
        status = ctypes.c_int()
        self._dll.GetStatus(ctypes.byref(status))
        return Status(status.value)

    # data

    def get_acquired_data(self, size: int, buffer: Union[ctypes.POINTER, ctypes.Array]):
        self._dll.GetAcquiredData(buffer, size)

    def get_latest_image(self, size: int, buffer: Union[ctypes.POINTER, ctypes.Array]):
        self._dll.GetMostRecentImage(buffer, size)


# utils

class ReturnCodeWrapper:
    def __init__(self, raw):
        self._raw = raw

    def __getattr__(self, item):
        value = getattr(self._raw, item, None)
        if not callable(value):
            return value

        def _func(*args, **kwargs):
            rc = value(*args, **kwargs)
            if rc != DRV_SUCCESS:
                raise ReturnCodeError(item, rc)
            return rc

        return _func


class ReturnCodeError(RuntimeError):
    def __init__(self, name: str, rc: int):
        super().__init__(f"function {name} failed with return code {rc}")
        self._rc = rc

    @property
    def rc(self) -> int:
        return self._rc


def find_dll_in_path(dll_path: str):
    if os.path.isabs(dll_path):
        return dll_path
    paths = os.environ.get('PATH', "")
    paths = paths.split(";")
    for path in paths:
        path = path.strip()
        if not path:
            continue
        dll_full_path = os.path.join(dll_path)
        if os.path.exists(dll_full_path) and os.path.isfile(dll_full_path):
            return dll_full_path
    return dll_path
